package com.tiny.tinyweb.core.utils;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.dbunit.dataset.Column;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.util.fileloader.FlatXmlDataFileLoader;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.beans.propertyeditors.CustomDateEditor;

import com.tiny.tinyweb.core.dao.entity.MapEntity;

/**
 * 使用dbunit进行单元测试时，从数据集文件创建相应的实体对象的工具类。
 *
 * @author wxiaobin
 */
public class DataSetUtils {
    /**
     * 根据数据集文件中数据表的所有记录创建一组具体实体类的对象，有异常发生的场合返回空对象。
     *
     * @param <T> 实体类的泛型约束
     * @param fileName 数据集文件的全路径
     * @param tableName 指定要处理的表名
     * @param entityClass 具体实体类
     * @return 基于每一条记录创建的实体对象的列表
     */
    public static <T extends MapEntity> List<T> createEntities(
            String fileName, String tableName, Class<T> entityClass) {
        try {
            List<Map<String, Object>> propertiesList = getProperties(fileName, tableName);
            List<T> entities = new ArrayList<T>();
            for (Map<String, Object> properties : propertiesList) {
                entities.add(wrapEntity(entityClass, properties));
            }
            return entities;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据数据集文件中数据表的第一条记录创建一个具体实体类的对象，通常该数据集文件中只有一条记录，
     * 有异常发生的场合返回空对象。
     *
     * @param <T> 实体类的泛型约束
     * @param fileName 数据集文件的全路径
     * @param tableName 指定要处理的表名
     * @param entityClass 具体实体类
     * @return 基于第一条记录创建的实体对象的列表
     */
    public static <T extends MapEntity> T createEntity(
            String fileName, String tableName, Class<T> entityClass) {
        try {
            return wrapEntity(entityClass, getProperties(fileName, tableName).get(0));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 从指定数据集文件中加载数据集，并创建指定表的属性列表，列表包含了该表的所有行，列表项是某一行的所有列。</br>
     * 例如，下面的数据集文件将生成test表的属性，而不会处理helo表。</br>
     * <code>
     * &lt;dataset&gt;</br>
     * &nbsp;&nbsp;&lt;test field01="1" field02="1" field03="1" /&gt;</br>
     * &nbsp;&nbsp;&lt;test field01="2" field02="2" field03="2" /&gt;</br>
     * &nbsp;&nbsp;&lt;helo field01="2" field02="2" field03="2" /&gt;</br>
     * &lt;/dataset&gt;</br>
     * </code>
     * 属性列表：[{field01=1,field02=1,field03=1},{field01=2,field02=2,field03=2}]
     *
     * @param fileName 数据集文件的全路径（相对于classpath）
     * @param tableName 指定要处理的表名
     * @return 指定表的属性列表
     * @throws IOException 数据集文件加载时的异常
     * @throws DataSetException dbunit数据集处理时的异常
     */
    private static List<Map<String, Object>> getProperties(String fileName, String tableName)
            throws IOException, DataSetException {
        // 从数据集文件加载dbunit数据集
        ReplacementDataSet expected = wrapDataSet(new FlatXmlDataFileLoader().load(fileName));
        // 从数据集中获得指定的表
        ITable table = expected.getTable(tableName);
        // 获得该表的列
        Column[] columns = table.getTableMetaData().getColumns();
        // 根据每一行的每一列创建其属性列表，列表项是一个列名为键，该行记该列的值为值的HashMap对象
        List<Map<String, Object>> propertiesList = new ArrayList<Map<String, Object>>();
        for (int i = 0; i < table.getRowCount(); i++) {
            // 创建该行的属性
            Map<String, Object> properties = new HashMap<String, Object>();
            for (Column c : columns) {
                // 将列名由下划线大写的命名方式转换为驼峰式，并将转换后的列名和该列的值装进Map
                properties.put(ConversionUtils.camelName(c.getColumnName()),
                        table.getValue(i, c.getColumnName()));
            }
            propertiesList.add(properties);
        }
        return propertiesList;
    }

    /**
     * 根据指定的属性包装一个实体对象。需要一些数据编辑器来完成格式化或类型转换处理，已知的数据编辑器如下。</br>
     * <ul>
     * <li>{@link org.springframework.beans.propertyeditors.CustomDateEditor} :
     * 将字符串转换为指定格式的日期类型</li>
     * </ul>
     * </p>
     *
     * @param <T> 实体类的泛型约束
     * @param entityClass 具体实体类
     * @param properties 某行记录的属性信息
     * @return 包装后的实体对象
     * @throws InstantiationException 实例化实体对象时的异常
     * @throws IllegalAccessException 实例化实体对象时的异常
     */
    private static <T extends MapEntity> T wrapEntity(Class<T> entityClass,
            Map<String, Object> properties) throws InstantiationException, IllegalAccessException {
        // 实例化一个实体对象
        T entity = entityClass.newInstance();
        // 获得该实体对象的Bean包装器
        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(entity);
        // 为类型转换注册属性编辑器
        bw.registerCustomEditor(Date.class, new CustomDateEditor(
                new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), true));
        // 根据该记录的属性信息给实体对象的字段设置值
        for (String key : properties.keySet()) {
            if (bw.isWritableProperty(key)) {
                bw.setPropertyValue(key, properties.get(key));
            }
        }
        return entity;
    }

    /**
     * 将dbunit的数据集包装成一个{@link org.dbunit.dataset.ReplacementDataSet}
     * 对象，以便加载数据集时做一些指定值的替换处理。一些已知的替换：</br>
     * <ul>
     * <li>[null]->null</li>
     * <li>[now]->current time</li>
     * </ul>
     *
     * @param dataset 包装前的数据集
     * @return 包装后的数据集
     */
    public static ReplacementDataSet wrapDataSet(IDataSet dataset) {
        ReplacementDataSet wrappedDataSet = new ReplacementDataSet(dataset);
        wrappedDataSet.addReplacementObject("[null]", null);
        wrappedDataSet.addReplacementObject("[now]", new Date(System.currentTimeMillis()));
        return wrappedDataSet;
    }
}
